/*
 * Copyright 2006-2008 Michele Mostarda ( michele.mostarda@gmail.com ).
 * All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the 'License');
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an 'AS IS' BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.graphnav.swing;

import org.graphnav.model.GraphModel;
import org.graphnav.model.ModifiableGraphModel;
import org.graphnav.model.event.GraphModelEvent;
import org.graphnav.model.event.GraphModelListener;
import org.graphnav.swing.event.EditingListener;
import org.graphnav.swing.event.GraphPanelEvent;
import org.graphnav.swing.event.GraphPanelHistoryListener;
import org.graphnav.swing.event.GraphPanelListener;
import org.graphnav.util.GeomUtils;

import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.TransferHandler;
import java.awt.*;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionAdapter;
import java.awt.geom.Rectangle2D;
import java.util.List;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

/**
 * This widget is able renderize a set of nodes and related arcs on the basis
 * of the content of a {@link org.graphnav.model.GraphModel}.
 */
public class GraphPanel extends JPanel {

    private class MouseEventsHandler extends MouseMotionAdapter implements MouseListener {

        /**
         * Set of selected shapes.
         */
        private Set selectedShapes;

        /**
         * Set of overlapped nodes.
         */
        private Set overlappedNodes;

        /**
         * Currently selected node.
         */
        private Node observedNode;

        /**
         * Currently selected arc.
         */
        private Arc observedArc;

        /**
         * Last position of a node.
         */
        private Point lastPosition;

        /**
         * Point mantaining the initial drag point.
         */
        private Point startDrag;

        private boolean dragStarted = false; // If dragStarted == true we're moving some shape.

        private boolean mousePressed = false;

        protected MouseEventsHandler() {
            this.selectedShapes = new HashSet() {

               public boolean add(Object obj) {
                   boolean result = super.add(obj);
                   notifyNodesSelection( new Node[] { (Node) obj } );
                   return result;
               }

               public void clear() {
                   Node[] previousNodes = (Node[]) toArray( new Node[size()] );
                   super.clear();
                   notifyNodesDeselection( previousNodes );
               }
            };

            this.overlappedNodes = new HashSet();
        }

        public void mouseClicked(MouseEvent me) {
            Point point = graphToCanvas(me);

            //Find for a clicked node.
            Node current;
            boolean nodeFound = false;
            for(int i = graphModel.nodesCount() - 1; i >= 0 ; i --) {
                current = graphModel.getNode(i);
                if(current.getBounds2D(detailLevel).contains(point)) {
                    nodeFound = true;
                    nodeClicked(current, me.getPoint(), me.getClickCount() >= 2);
                    break;
                }
            }

            if( nodeFound ) {
                return;
            }

            // Find for a clicked arc.
            Arc arc = findArc(me);
            if(arc != null) {
                arcClicked(arc, point,  me.getClickCount() >= 2);
                return;
            }

            //selectedShapes.clear();
            panelClicked(point);
        }

        public void mouseEntered(MouseEvent e) { }

        public void mouseExited(MouseEvent e) { }

        public void mousePressed(MouseEvent me) {
            mousePressed = true;
            lastPosition = graphToCanvas(me);
            Node current;
            boolean nodeFound = false;
            for(int i = graphModel.nodesCount() - 1; i >= 0 ; i --) {
                current = graphModel.getNode(i);
                if(current.getBounds2D(detailLevel).contains(lastPosition)) { // A node found.
                    nodeFound   = true;
                    dragStarted = true;
                    if( selectedShapes.contains(current) ) { //The node is already selected (so we want to drag it).
                       //Do nothing.
                    } else { //We're selecting single item.
                        selectedShapes.clear();
                        selectedShapes.add(current);
                        //TODO: optimize this.
                        repaint();
                    }
                    break;
                }
            }
            if( ! nodeFound ) { //If no nodes are located, the selected shapes are cleared.
                selectedShapes.clear();
                repaint();
            }
            startDrag = lastPosition;
        }

        public void mouseReleased(MouseEvent me) {
            mousePressed = false;
            if(dragStarted) {
                dragStarted   = false;
                if(selectedShapes.size() == 0 || overlappedNodes.size() == 0) { return; }
                Node[] sNodes = (Node[]) selectedShapes.toArray( new Node[selectedShapes.size()] );
                Node[] oNodes = (Node[]) overlappedNodes.toArray( new Node[overlappedNodes.size()] );
                addArc(sNodes[0], oNodes[0]);
                notifyNodesOverlapped(sNodes, oNodes);
                clearOverlappedNodes();
            }

            lastPosition  = null;
            startDrag     = null;

            //Optimize this.
            repaint();
        }

        public void mouseDragged(MouseEvent me) {

            //Intercepting dragging events.
            //System.out.println("x:" + me.getX() + "y:" + me.getY());
            if( me.getX() < 0  && ( me.getY() > 0 && me.getY() < drawingArea.getHeight() ) ) {
                //me.consume();

                //This is a drag, not a click.
                JComponent c = (JComponent) me.getSource();
                //Tell the transfer handler to initiate the drag.
                TransferHandler handler = c.getTransferHandler();
                handler.exportAsDrag(c, me, TransferHandler.COPY);
                System.out.println("done");
            }


            Point mePoint = graphToCanvas(me);
            if( dragStarted && ! selectedShapes.isEmpty() ) { //We are moving a selection.

                int dx = (int) ( mePoint.getX() - lastPosition.getX() );
                int dy = (int) ( mePoint.getY() - lastPosition.getY() );
                Node current = null;
                for( Iterator shapes = selectedShapes.iterator(); shapes.hasNext(); ) {
                    current = (Node) shapes.next();
                    current.move(dx, dy);
                }

                computeOverlappedNodes(overlappedNodes);

                notifyNodesMoved( (Node[]) selectedShapes.toArray( new Node [selectedShapes.size()] ) );

            } else {  // We are performing a multi selection.

                Node current;
                selectedShapes.clear();
                Rectangle2D selectionArea = getSelectionArea();
                for(int i = graphModel.nodesCount() - 1; i >= 0 ; i --) {
                    current = graphModel.getNode(i);
                    if(selectionArea.intersects(current.getBounds2D(detailLevel))) {
                        selectedShapes.add(current);
                    }
                }
                
            }

            lastPosition = mePoint;

            //TODO: optimize!
            repaint();
        }

        public void mouseMoved(MouseEvent me) {

            boolean somethingChanged = false;
            if(observedArc != null || observedNode != null) { somethingChanged = true; }
            observedNode = null;
            observedArc  = null;

            if( getDetailLevel() == Renderable.DETAIL_LEVEL_MINIMAL ) {
                observedNode = findNode(me);
            }

            if(observedNode == null) {
                observedArc = findArc(me);
                // TODO:
                //Rectangle2D container = arc.getFromNode().getBounds2D(getDetailLevel()).createIntersection( arc.getToNode().getBounds2D(getDetailLevel()) );
                //repaint(container.getBounds());
            }

            if(somethingChanged || observedNode != null || observedArc != null) { repaint(); }
        }

        public Rectangle2D getSelectionArea() {
            return mousePressed ? getMinimumContainingRectangle(startDrag, lastPosition) : null;
        }

        public boolean isObserved(Node n) {
            return observedNode == null ? false : observedNode == n;
        }

        public Node getObservedNode() {
            return observedNode;
        }

        public boolean isObserved(Arc a) {
            return observedArc == null ? false : observedArc == a;
        }

        public Arc getObservedArc() {
            return observedArc;
        }

        public boolean isSelected(Node n) {
            return selectedShapes.contains(n);
        }

        public boolean isOverlapped(Node n) {
            return overlappedNodes.contains(n);
        }

        public void setSelectedNode(Node n) {
            selectedShapes.clear();
            selectedShapes.add(n);
        }

        public Node getSelectedNode() {
            Iterator nodes = selectedShapes.iterator();
            return nodes.hasNext() ? (Node) nodes.next() : null;
        }

        private boolean singleNodeSelected() {
            return selectedShapes.size() == 1;
        }

        private Arc findArc(MouseEvent me) {
            Iterator arcs = getModel().arcs();
            Arc current;
            while(arcs.hasNext()) {
                current = (Arc) arcs.next();
                Point mePoint = graphToCanvas(me);
                int detailLevel = getDetailLevel();
                Rectangle2D fromNodeBounds = current.getFromNode().getBounds2D(detailLevel);
                Rectangle2D toNodeBounds   = current.getToNode().getBounds2D(detailLevel);
                if(
                    GeomUtils.inRange(
                        fromNodeBounds.getCenterX(),
                        fromNodeBounds.getCenterY(),
                        toNodeBounds.getCenterX(),
                        toNodeBounds.getCenterY(),
                        mePoint.getX(),
                        mePoint.getY(),
                        10
                    )
                ){
                    //System.out.println("near arc " + current);
                    return current;
                }
            }
            return null;
        }

        private Node findNode(MouseEvent me) {
            Node current;
            int dl = getDetailLevel();
            Point mp = graphToCanvas(me);
            for(int i = getModel().nodesCount() -1; i >= 0;  i--) {
                current = getModel().getNode(i);
                if(current.getBounds2D(dl).contains(mp)) {
                    return current;
                }
            }
            return null;
        }

        private Set computeOverlappedNodes(Set toRecycle) {
            toRecycle.clear();
            Node n;
            for(int i = 0; i < graphModel.nodesCount(); i++) {
                n = graphModel.getNode(i);
                if( computeOverlapping(n) ) {
                    toRecycle.add(n);
                }
            }
            return toRecycle;
        }

        private void clearOverlappedNodes() {
            overlappedNodes.clear();
        }

        //TODO: optimize this algorithm.
        private boolean computeOverlapping(Node node) {
            Node current = null;
            for(int i = graphModel.nodesCount() - 1; i >= 0 ; i --) {
                current = graphModel.getNode(i);
                if( current.equals(node) || ! selectedShapes.contains(current)) { // Every node is always overlapped with itself.
                    continue;
                }
                if(current.getBounds2D(detailLevel).intersects(node.getBounds2D(detailLevel))) {
                    return true;
                }
            }
            return false;
        }

        private Rectangle2D getMinimumContainingRectangle(Point p1, Point p2) {
            if(startDrag == null || dragStarted) {
                return null;
            }
            //System.out.println("p1: " + p1);
            //System.out.println("p2: " + p2);
            double minX = Math.min(p1.getX(), p2.getX());
            double minY = Math.min(p1.getY(), p2.getY());
            double maxX = Math.max(p1.getX(), p2.getX());
            double maxY = Math.max(p1.getY(), p2.getY());
            return new Rectangle2D.Double(minX, minY, maxX - minX, maxY - minY);
        }
    }

    private class HistoryManager {

        class HistoryElem {
            
            Node node;
            int  mode;

            HistoryElem(Node n, int m) {
                node = n;
                mode = m;
            }
        }

        private int MAX_HISTORY_SIZE = 20;

        private List navigationHistory = new ArrayList(20);

        private List listeners = new ArrayList();

        int index = -1;

        void pushNode(Node n, int m) {
            if(navigationHistory.size() == MAX_HISTORY_SIZE) {
                navigationHistory.remove(0);
            }
            navigationHistory.add(new HistoryElem(n,m));
            index = navigationHistory.size() - 1;
            notifyElementAdded();
        }

        HistoryElem prev() {
            index--;
            if(index == -1) {
                return null;
            }
            return (HistoryElem) navigationHistory.get(index);
        }

        HistoryElem next() {
            index++;
            if(index == navigationHistory.size()) {
                return null;
            }
            return (HistoryElem) navigationHistory.get(index);
        }

        HistoryElem moveLast() {
            if(isEmpty()) {return null;}
            index = navigationHistory.size() - 1;
            return (HistoryElem) navigationHistory.get(index);
        }

        HistoryElem moveFirst() {
            if(isEmpty()) {return null;}
            index = 0;
            return (HistoryElem) navigationHistory.get(0);
        }

        boolean hasPrev() {
            return index > 0;
        }

        boolean hasNext() {
            System.out.println("index: " + index);
            System.out.println("size -1: " +  ( navigationHistory.size() - 1) );
            return index < navigationHistory.size() - 1;
        }

        boolean isEmpty() {
            return navigationHistory.isEmpty();
        }

        void addListener(GraphPanelHistoryListener gphl) {
            listeners.add(gphl);
        }

        void removeListener(GraphPanelHistoryListener gphl) {
            listeners.remove(gphl);
        }

        private  void notifyElementAdded() {
            GraphPanelHistoryListener gphl;
            for(Iterator iter = listeners.iterator(); iter.hasNext(); ) {
                gphl = (GraphPanelHistoryListener) iter.next();
                gphl.elementAdded();
            }
        }
    }

    public static final int VIEW_ALL = 0;

    public static final int NAVIGATE = 1;

    private Point translation = new Point(0, 0);

    private GraphModel graphModel;

    private EffectProcessor effectProcessor;

    private MouseEventsHandler dndListener;

    private List graphPanelListeners;

    private int detailLevel = Renderable.DETAIL_LEVEL_MEDIUM;

    private HistoryManager historyManager = new HistoryManager();

    private int mode = VIEW_ALL;

    private Dimension drawingArea = new Dimension();

    private boolean graphicsEnhanced = true;

    public GraphPanel(GraphModel gm, EffectProcessor fp) {
        init(gm, fp);
    }

    public GraphPanel(GraphModel gm) {
        DefaultFadeProcessor fp = new DefaultFadeProcessor(this, gm);
        init(fp, fp);
    }

    private void init(GraphModel gm, EffectProcessor fp) {
        graphModel = gm;
        graphModel.addGraphModelListener(
            new GraphModelListener() {
                public void modelChanged(GraphModelEvent gme) {
                    repaint();
                }
            }
        );

        effectProcessor = fp;

        dndListener = new MouseEventsHandler();
        graphPanelListeners = new ArrayList();

        //Adding internal panel selection istener.
        addGraphPanelListener(iel.igpl);

        addMouseListener(dndListener);
        addMouseMotionListener(dndListener);
        setBackground(Color.WHITE);

        setTransferHandler( new TransferHandler("selectedNode") );
    }

    public GraphModel getModel() {
        return graphModel;
    }

    public int getDetailLevel() {
        return detailLevel;
    }

    public void setDetailLevel(int dl) {
        if(detailLevel == dl) { return; }
        
        if(dl == Renderable.DETAIL_LEVEL_MEDIUM || dl == Renderable.DETAIL_LEVEL_MINIMAL) {
            detailLevel = dl;
            repaint();
        } else {
            throw new IllegalArgumentException("Invalid detail level: " + dl);
        }
    }

    public int getMode() {
        return mode;
    }

    public void setMode(int m) {
        if(mode == m) {
            return;
        }
        
        if(m == VIEW_ALL) {
            effectProcessor.disableEnhancement();
        } else if(m == NAVIGATE) {
            //EMPTY.
        } else {
           throw new IllegalArgumentException(); 
        }
        mode = m;
    }

    public void arrange(NodeArranger na, Object options) {
        na.arrange(getModel(), getDetailLevel(), null, getFadeProcessor(), options);
    }

    public void addGraphPanelListener(GraphPanelListener gpl) {
        graphPanelListeners.add(gpl);
    }

    public void removeGraphPanelListener(GraphPanelListener gpl) {
        graphPanelListeners.remove(gpl);
    }


    public Dimension getPreferredSize() {
        return drawingArea;
    }

    public void paintComponent(Graphics g) {

        super.paintComponent(g);

        Graphics2D g2d = (Graphics2D) g;

        // Manages the graphics enhancement.
        if(graphicsEnhanced) {

            g2d.setRenderingHint(
                    RenderingHints.KEY_ALPHA_INTERPOLATION,
                    RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY
            );
            g2d.setRenderingHint(
                    RenderingHints.KEY_COLOR_RENDERING,
                    RenderingHints.VALUE_COLOR_RENDER_QUALITY
            );
            g2d.setRenderingHint(
                    RenderingHints.KEY_DITHERING,
                    RenderingHints.VALUE_DITHER_ENABLE
            );
            g2d.setRenderingHint(
                    RenderingHints.KEY_FRACTIONALMETRICS,
                    RenderingHints.VALUE_FRACTIONALMETRICS_ON
            );
            g2d.setRenderingHint(
                    RenderingHints.KEY_INTERPOLATION,
                    RenderingHints.VALUE_INTERPOLATION_BICUBIC
            );
            g2d.setRenderingHint(
                    RenderingHints.KEY_RENDERING,
                    RenderingHints.VALUE_RENDER_QUALITY
            );
            g2d.setRenderingHint(
                    RenderingHints.KEY_DITHERING,
                    RenderingHints.VALUE_DITHER_ENABLE
            );
            g2d.setRenderingHint(
                    RenderingHints.KEY_STROKE_CONTROL,
                    RenderingHints.VALUE_STROKE_PURE
            );
            g2d.setRenderingHint(
                    RenderingHints.KEY_TEXT_ANTIALIASING,
                    RenderingHints.VALUE_TEXT_ANTIALIAS_ON
            );

        }

//        ///////
//        for(int i = 0; i < 100; i++) {
//            Point p = org.graphnav.util.GeomUtils.getRoundPosition(getWidth() / 2, getHeight() / 2, 100, i);
//            g.drawRect((int)p.getX(), (int)p.getY(), 5, 5);
//        }
//        if(true)return;
//        ///////

        g2d.translate(translation.getX(), translation.getY());        

        Arc currentArc;
        Node currentNode;

        GraphModel gm = getModel();

        gm.startVisit();

        //Drawing arcs first.
        for( Iterator arcs = gm.arcs(); arcs.hasNext(); ) {
            currentArc = (Arc) arcs.next();
            //if(currentRenderable.getFromNode().isCollapsed()) { continue; }
            currentArc.paintShape(g2d, dndListener.isObserved(currentArc), false, detailLevel);
        }

        Dimension newBounds = new Dimension();

        //Drawing nodes.
        for( Iterator nodes = gm.nodes(); nodes.hasNext(); ) {
            currentNode = (Node) nodes.next();

            // Skipping the observed node.
            if( dndListener.isObserved(currentNode) ) { continue; }

            mergeBounds(newBounds, currentNode.getBounds2D(detailLevel));

            //if(currentNode.isCollapsed()) { continue; }
            currentNode.paintShape(
                    g2d,
                    dndListener.isSelected(currentNode),
                    dndListener.isOverlapped(currentNode),
                    detailLevel
            );
        }

        // Painting the observed node on top of everithing.
        if( dndListener.getObservedNode() != null ) {
            dndListener.getObservedNode().paintShape(
                    g2d,
                    dndListener.isSelected(dndListener.getObservedNode()),
                    dndListener.isOverlapped(dndListener.getObservedNode()),
                    Renderable.DETAIL_LEVEL_MEDIUM
            );
        }

        gm.endVisit();

        //Resizing the dimensional bounds.
        if( ! drawingArea.equals(newBounds) ) {
            drawingArea.setSize(newBounds);
            notifyPanelResized(newBounds);
        }

        //Draw selection rectangle.
        Rectangle2D selection = dndListener.getSelectionArea();
        if(selection != null) {
            g2d.setClip(getBounds());
            g2d.setStroke(Renderable.THIN_STROKE);
            g2d.setColor(Color.BLACK);
            g2d.draw(selection);
            g2d.setColor(Color.GRAY);
            g2d.setComposite( AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.7f));
            g2d.fill(selection);
        }

        //Test
//        JButton bt = new JButton("Test button!");
//        bt.setSize(100, 100);
//        AffineTransform oldTransform = g2d.getTransform();
//        g2d.translate(200,200);
//        g2d.rotate(45);
//        bt.paint(g);
//        g2d.setTransform(oldTransform);
        //////

        g2d.dispose();
    }

    private void mergeBounds(Dimension dim, Rectangle2D bounds) {
        double boundsWidth  = bounds.getX() + bounds.getWidth();
        double boundsHeight = bounds.getY() + bounds.getHeight();
        double x = dim.getWidth() >  boundsWidth ? dim.getWidth() : boundsWidth;
        double y = dim.getHeight() >  boundsHeight ? dim.getHeight() : boundsHeight;
        dim.setSize(x,y);
    }

    protected Point graphToCanvas(MouseEvent me) {
        Point point = new Point(me.getPoint());
        point.translate( - (int) translation.getX(), - (int) translation.getY());
        return point;
    }

    public double getTranslationX() {
        return translation.getX();
    }

    public double getTranslationY() {
        return translation.getY();
    }

    public void nextHistoryElement() {
        if(historyManager.hasNext()) {
            HistoryManager.HistoryElem hm = historyManager.next();
            setMode(hm.mode);
            setSelectedNode(hm.node);
        }
    }

    public void prevHistoryElement() {
        if(historyManager.hasPrev()) {
            HistoryManager.HistoryElem hm = historyManager.prev();
            setMode(hm.mode);
            setSelectedNode(hm.node);
        }
    }

    public boolean hasNextHistoryElement() {
        return historyManager.hasNext();
    }

    public boolean hasPreviousHistoryElement() {
        return historyManager.hasPrev();
    }

    public void addHistoryListener(GraphPanelHistoryListener gphl) {
        historyManager.addListener(gphl);
    }

    public void removeHistoryListener(GraphPanelHistoryListener gphl) {
        historyManager.removeListener(gphl);        
    }

    public void setSelectedNode(Node n) {
        dndListener.setSelectedNode(n);
        effectProcessor.enhanceNode(n, getMode() == NAVIGATE ? true : false, true);
    }

    public void setGraphicsEnhanced(boolean flag) {
        graphicsEnhanced = flag;
        repaint();
    }

    public boolean isGraphicsEnhanced() {
        return graphicsEnhanced;
    }

    protected EffectProcessor getFadeProcessor() {
        return effectProcessor;
    }

    protected void setViewLocation(double x, double y) {
        translation.setLocation(x, y);
        repaint();
    }

    protected void translateViewLocation(double dx, double dy) {
        translation.translate((int) dx, (int) dy);
        repaint();
    }

    protected void nodeClicked(Node n, Point p, boolean edited) {
        if(edited) { // Node editing.
            editNode(n, p);
        } else { // Node navigation.
            selectNode(n);
        }
    }

    protected void selectNode(Node n) {
        historyManager.pushNode(n, getMode());
        if(mode == NAVIGATE) {
            effectProcessor.enhanceNode(n, true, true);
        } else { //VIEW_ALL.
            effectProcessor.shiftView(
                n.getBounds2D(getDetailLevel()).getCenterX(),
                n.getBounds2D(getDetailLevel()).getCenterY()
            );
        }
    }

    protected void arcClicked(Arc a, Point p, boolean edited) {
        if(edited) {
            editArc(a, p);
        } else {
            followArc(a);
        }
    }

    protected void panelClicked(Point p) {
        effectProcessor.shiftView(p.getX(), p.getY());
    }

    public class IntenalEditingHandler implements EditingListener {

        private JDialog editingDialog;

        private AbstractRenderable currentRenderable;

        protected IntenalEditingHandler() {
            editingDialog = new JDialog((JFrame) javax.swing.SwingUtilities.getWindowAncestor(GraphPanel.this), true);
            editingDialog.setUndecorated(true);
            //editingDialog.setDefaultCloseOperation();
        }

        public void showEditor(AbstractRenderable a, Point p) {
            currentRenderable = a;
            RenderableEditor re = a.edit(iel);

            editingDialog.getContentPane().removeAll();
            editingDialog.getContentPane().setLayout(new BorderLayout());
            editingDialog.getContentPane().add(re, BorderLayout.CENTER);
            editingDialog.setLocation(p);
            editingDialog.pack();
            editingDialog.setVisible(true);
        }

        public void showEditor(Node fromNode, Node toNode) {
            fromNode.editNewArc(toNode, iel);
        }

        public void editingSucceded() {
            editingDialog.setVisible(false);
            //Rectangle2D fromNodeBounds = currentRenderable.getFromNode().getBounds2D(getDetailLevel());
            //Rectangle2D toNodeBounds = currentRenderable.getFromNode().getBounds2D(getDetailLevel());
            //Rectangle2D repaintArea = fromNodeBounds.createUnion(toNodeBounds);
            //repaint(repaintArea.getBounds());
            repaint();
        }

        public void editingRejected() {
            editingDialog.setVisible(false);
        }

        public void deletionSucceded() {
            editingDialog.setVisible(false);
            if(getModel() instanceof ModifiableGraphModel ) {
                ModifiableGraphModel mgm = (ModifiableGraphModel) getModel();
                if(currentRenderable instanceof Node) {
                    mgm.removeNode((Node) currentRenderable);
                } else {
                    mgm.removeArc((Arc) currentRenderable);
                }
            } else {
                throw new IllegalStateException();
            }
        }

        private InternalGraphPanelListener igpl = new InternalGraphPanelListener();

        public void addArcRequired(Node fromNode) {
            editingDialog.setVisible(false);
            igpl.init(fromNode);
        }

        public void addArcRequired(Node fromNode, Node toNode, Object arcData) {
            Arc newArc = ( (ModifiableGraphModel) getModel()).createArc(fromNode, toNode, arcData);
            ( (ModifiableGraphModel) getModel()).addArc(newArc);
        }

        protected class InternalGraphPanelListener implements GraphPanelListener {

            private Node fromNode;

            private boolean active = false;

            protected void init(Node fn) {
                fromNode = fn;
                active = true;
            }

            public void nodesSelected(GraphPanelEvent gpe) {
                if(! active) { return; }

                String res = JOptionPane.showInputDialog("Arc name?");
                if(res == null) { return; }
                Arc newArc = ( (ModifiableGraphModel) getModel()).createArc(fromNode, gpe.getSelectedNodes()[0], res);
                ( (ModifiableGraphModel) getModel()).addArc(newArc);
                active = false;
            }

            public void nodesDeselected(GraphPanelEvent gpe) {}

            public void nodesMoved(GraphPanelEvent gpe) {}

            public void nodesOverlapped(GraphPanelEvent gpe) {}

            public void nodeCollapsed(GraphPanelEvent gpe) {}

            public void nodeExpanded(GraphPanelEvent gpe) {}

            public void panelResized(GraphPanelEvent gpe) {}
        }

    }

    private IntenalEditingHandler iel = new IntenalEditingHandler();

    private void editNode(Node toEdit, Point p) {
        Point frameLocation = javax.swing.SwingUtilities.getWindowAncestor(this).getLocation();
        Point nodeLocationInFrame = new Point(
                (int) ( frameLocation.getX() + p.getX() ),
                (int) ( frameLocation.getY() + p.getY() )
        );
        iel.showEditor(toEdit, nodeLocationInFrame);
    }

    // Handles the arc edititng.
    private void editArc(Arc toEdit, Point p) {
        iel.showEditor(toEdit, p);
    }

    //handles an arc selection.
    private void followArc(Arc a) {
        selectNode( a.getToNode() );
    }

    public void addArc(Node fromNode, Node toNode) {
        iel.showEditor(fromNode, toNode);
    }

    private void notifyNodesSelection(Node[] nodes) {
        GraphPanelListener gpl;
        for(Iterator listeners = graphPanelListeners.iterator(); listeners.hasNext(); ) {
            gpl = (GraphPanelListener) listeners.next();
            gpl.nodesSelected( new GraphPanelEvent(this, nodes) );
        }
    }

    private void notifyNodesDeselection(Node[] nodes) {
        GraphPanelListener gpl;
        for(Iterator listeners = graphPanelListeners.iterator(); listeners.hasNext(); ) {
            gpl = (GraphPanelListener) listeners.next();
            gpl.nodesDeselected( new GraphPanelEvent(this, nodes) );
        }
    }

    private void notifyNodesMoved(Node[] nodes) {
        GraphPanelListener gpl;
        for(Iterator listeners = graphPanelListeners.iterator(); listeners.hasNext(); ) {
            gpl = (GraphPanelListener) listeners.next();
            gpl.nodesMoved( new GraphPanelEvent(this, nodes) );
        }
    }

    private void notifyNodesOverlapped(Node[] selectedNodes, Node[] overlappedNodes) {
        GraphPanelListener gpl;
        for(Iterator listeners = graphPanelListeners.iterator(); listeners.hasNext(); ) {
            gpl = (GraphPanelListener) listeners.next();
            gpl.nodesMoved( new GraphPanelEvent(this, selectedNodes, overlappedNodes) );
        }
    }

    private void notifyPanelResized(Dimension bounds) {
        GraphPanelListener gpl;
        for(Iterator listeners = graphPanelListeners.iterator(); listeners.hasNext(); ) {
            gpl = (GraphPanelListener) listeners.next();
            gpl.panelResized( new GraphPanelEvent(this, bounds) );
        }
    }


}
